#! /usr/bin/env python
"""Update the version after a commit or an update.

Add the following lines to .hg/hgrc:
[hooks]
commit.version = /bin/sh -c "`hg root`/version-hook.py commit clewn/__version__.py"
update.version = /bin/sh -c "`hg root`/version-hook.py update clewn/__version__.py"
"""

import sys
import os
import optparse
import subprocess

python3 = sys.version_info >= (3, 0)
if python3:
    _zip = zip
else:
    import itertools
    _zip = itertools.izip

pgm = os.path.basename(sys.argv[0])

class HookError(Exception):
    """Base class for version-hook exceptions."""

def indent(str, width=4):
    """Indent a set of lines."""
    return '\n'.join([width * ' ' + x.strip() for x in str.split('\n')])

def command(cmd_list):
    """Run a command."""
    out, err = subprocess.Popen(cmd_list,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE).communicate()
    if err:
        raise HookError(err)
    if sys.version_info >= (3, 0):
        return out.strip().decode()
    return out.strip()

def hook_type(args):
    """Return the hook type."""
    allowed = ('commit', 'update')
    if not args:
        raise optparse.OptionValueError(
                            '%s: missing %s hook type' % (pgm, allowed))
    htype = args[0]
    if htype not in allowed:
        raise optparse.OptionValueError(
                    '%s: "%s" is not a %s hook type' % (pgm, htype, allowed))
    return htype

def fullpath(filename):
    """Return 'filename' full pathname"""
    try:
        rootdir = fullpath.rootdir
    except AttributeError:
        rootdir = fullpath.rootdir = command(['hg', 'root'])
    return os.path.join(rootdir, filename)

def sort_tags(tags):
    """Sort 'major.minor.version' tags in decreasing order."""
    tags = map(lambda x: (int(x[0]), int(x[1]), x[2]),
            (s for s in (t.split('.') for t in tags) if len(s) == 3))
    for tag in sorted(tags, reverse=True):
        yield '.'.join(str(x) for x in tag)

def version(args, debug):
    """Return the version."""
    htype = hook_type(args)
    node = parent = os.environ['HG_PARENT1']
    if htype == 'commit':
        node = os.environ['HG_NODE']
    if debug:
        sys.stderr.write('node-parent: %s %s\n' % (str(node), str(parent)))

    # build a tags map after removing alphanumeric tags
    taglist = command(['hg', 'tags', '--debug']).split()
    it = iter(taglist)
    tagmap = dict(_zip(it, it))
    alphatags = []
    if python3:
        items = tagmap.items()
    else:
        items = tagmap.iteritems()
    for tag, nodeid in items:
        if tag[0].isalpha():
            alphatags.append(tag)
        else:
            tagmap[tag] = nodeid[nodeid.index(':')+1:]
    for tag in alphatags:
        del tagmap[tag]

    # browse the tags in lexical decreasing order
    for tag in sort_tags(tagmap):
        nodeid = tagmap[tag]
        if debug:
            sys.stderr.write('tag-nodeid: %s %s\n' % (str(tag), str(nodeid)))
        if nodeid.find(parent) == 0:
            # update: updating to 'tag'
            if htype == 'update':
                return tag, ''
            # commit: commiting a new 'tag' (running the tag command)
            else:
                newtag = command(['hg', 'diff',
                                    fullpath('.hgtags'), '-c',  'tip'])
                if newtag != '':
                    if debug:
                        sys.stderr.write('newtag:\n%s\n' % indent(newtag))
                    return tag, ''
                elif debug:
                    sys.stderr.write('newtag: ""\n')
        ancestor = command(['hg',  'debugancestor',  nodeid, node])
        if debug:
            sys.stderr.write('ancestor-nodeid: %s %s\n'
                                    % (str(ancestor), str(nodeid)))
        if ancestor[ancestor.index(':')+1:] == nodeid:
            return tag, node

    return 'unknown', ''

if __name__ == '__main__':
    if os.environ.get('HG_ERROR') == '1':
        sys.exit(0)

    # abort when mq patches are being applied, to avoid error:
    #   mq status file refers to unknown node ddd...
    #   warning: commit.version hook exited with status 1
    try:
        if command(['hg', 'qapplied']):
            sys.exit(0)
    except HookError:
        # mq extension not installed
        pass

    parser = optparse.OptionParser(usage=
                    'usage: %prog [options] commit | update [filename]\n\n'
                     + globals()['__doc__'])
    parser.add_option('-d', '--debug', action="store_true", default=False,
                        help='print debugging information on stderr')
    options, args = parser.parse_args()

    try:
        tag, changeset = version(args, options.debug)
        if len(args) == 2:
            f = open(fullpath(args[1]), 'w')
            f.write('# this file is autogenerated by %s\n' % pgm)
            f.write('tag = "%s"\n' % tag)
            f.write('changeset = "%s"\n' % changeset)
            f.close()
        else:
            sys.stdout.write('version = %s.%s\n' % (tag, changeset))
    except (HookError, optparse.OptionValueError):
        sys.stderr.write('%s\n'  % sys.exc_info()[1])
        sys.exit(1)

